Skip to content

Conversation

@jimm98y
Copy link
Collaborator

@jimm98y jimm98y commented Jan 4, 2026

To remove the AGPL-derived code, I implemented a new DTLS-SRTP layer based upon BouncyCastle DTLS samples and the RFCs. More info here: https://github.com/jimm98y/SharpSRTP

This PR wires up the new implementation into the current code base. The only modification when compared to SharpSRTP are the namespaces which have been adjusted to match sipsorcery and the logging was wired up to sipsorcery's logging mechanism.

I have unit tests for all the implemented RFCs in my repo and it should be easy to sync any changes/bugfixes in the future. I tested some of the WebRTC samples that I was able to run on ARM64 and I also tested the new implementation on https://github.com/jimm98y/SharpRTSPtoWebRTC.

This new DTLS-SRTP implementation also fixes the RSA TLS certificate sipsorcery was using in WebRTC, now it's using ECDsa by default. By default I also disabled DTLS 1.0, leaving only DTLS 1.2 available. DTLS 1.0 can be enabled using overrides. DTLS 1.3 currently cannot be supported because BouncyCastle does not support it yet.

This PR should also resolve AES-GCM AEAD support #871.

@sipsorcery
Copy link
Member

No probs at all. This is tricky stuff. I'll pull the changes and run the tests again. In theory most of the WebRTC implementations should work with both RSA and ECDSA but in the past I have observed some only support one with the shift being from RSA to ECDSA.

@sipsorcery
Copy link
Member

FireFox now working for the get started app.

All but the two docker tests below are now able to get a clean DTLS connection.

aiortc test:
docker run -it --rm --init -p 8080:8080 ghcr.io/sipsorcery/aiortc-webrtc-echo

Exception on sipsrocery client:

[17:11:55 DBG] RTCPeerConnection DoDtlsHandshake started.
[17:11:55 DBG] DTLS server negotiated DTLS 1.2
[17:11:55 DBG] DTLS server received client certificate chain of length 1
[17:11:55 DBG]     fingerprint:SHA-256 4A:DF:CC:45:35:71:63:BF:75:29:F1:71:1D:FC:E5:07:5F:82:56:8B:02:4B:15:7D:B3:2A:80:84:00:96:E5:CC (CN=c35af299c54d3ac96be0bc0ef1bfdc99)
[17:11:55 DBG] Server 'tls-server-end-point': 0a17e3244744d840e96ce90f5b2202914cf4538d6339f6ab17cef36dbffaeb36
[17:11:55 DBG] Server 'tls-unique': ba45909af69a7b99680c628c
[17:11:55 DBG] RTCPeerConnection DTLS handshake result True, is handshake complete True.
[17:11:55 WRN] RTCPeerConnection DTLS handshake failed. Object reference not set to an instance of an object.
System.NullReferenceException: Object reference not set to an instance of an object.
   at SIPSorcery.Net.RTCPeerConnection.DoDtlsHandshake(DtlsSrtpTransport dtlsHandle) in c:\dev\sipsorcery\src\net\WebRTC\RTCPeerConnection.cs:line 1756
   at SIPSorcery.Net.RTCPeerConnection.<IceConnectionStateChange>b__128_0() in c:\dev\sipsorcery\src\net\WebRTC\RTCPeerConnection.cs:line 474
   at System.Threading.Tasks.Task`1.InnerInvoke()
   at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
   at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread)
--- End of stack trace from previous location ---
   at SIPSorcery.Net.RTCPeerConnection.IceConnectionStateChange(RTCIceConnectionState iceState) in c:\dev\sipsorcery\src\net\WebRTC\RTCPeerConnection.cs:line 474
[17:11:55 DBG] Peer connection closed with reason dtls handshake failed.

sipsorcery master test:
docker run -it --rm --init -p 8080:8080 ghcr.io/sipsorcery/sipsorcery-webrtc-echo

DTLS handshake succeeeds but then the server (which is sipsorcery master branch code) gets a bunch of unprotect errors.

[17:15:53 DBG] Starting DLS handshake with role active.
[17:15:53 DBG] RTCPeerConnection DoDtlsHandshake started.
[17:15:53 DBG] DTLS commencing handshake as client.
[17:15:53 DBG] RTCPeerConnection DTLS handshake result True, is handshake complete True.
[17:15:53 DBG] RTCPeerConnection remote certificate fingerprint matched expected value of E7:B2:1F:88:DB:7D:90:30:3F:3E:40:2E:39:C1:AE:23:E2:C5:E3:F0:A3:C7:71:1B:E9:84:CC:0E:30:54:83:12 for sha-256.
[17:15:53 INF] Peer connection state changed to connected.
[17:15:53 DBG] Starting RTCP session for aa78c23e-847c-4c89-9aee-d63f32842577.
[17:15:53 DBG] Set remote track (audio - index=0) SSRC to 1964527005 remote RTP endpoint 192.168.1.123:56742.
[17:15:53 WRN] SRTP unprotect failed for audio, result -1.
[17:15:53 DBG] SCTP INIT packet received, initial tag 2264145410, initial TSN 4020604427.
[17:15:53 WRN] SRTP unprotect failed for audio, result -1.
[17:15:53 WRN] SRTP unprotect failed for audio, result -1.
[17:15:53 DBG] Cookie: {"SourcePort":5000,"DestinationPort":5000,"RemoteTag":2264145410,"RemoteTSN":4020604427,"RemoteARwnd":262144,"RemoteEndPoint":"","Tag":2882525985,"TSN":2786180563,"ARwnd":262144,"CreatedAt":"2026-01-11T17:15:53.3431722+00:00","Lifetime":60,"HMAC":"5C569C5905DA75ABC42DC1EF809D91AABF940FB643101AFBBA7C1C079B4979DD"}
[17:15:53 DBG] SCTP transport successfully connected.
[17:15:53 WRN] SRTP unprotect failed for audio, result -1.
[17:15:53 DBG] SCTP association data send thread started for association 5000:5000:33854.
[17:15:53 WRN] SRTP unprotect failed for audio, result -1.
[17:15:53 WRN] SRTP unprotect failed for audio, result -1.
[17:15:53 DBG] DCEP OPEN channel type 0, priority 0, reliability 0, label dcx, protocol null.
[17:15:53 INF] WebRTC new data channel opened by remote peer for stream ID 1, type DATA_CHANNEL_RELIABLE, priority 0, reliability 0, label dcx, protocol null.
[17:15:53 INF] Data channel opened for label dcx, stream ID 1.
[17:15:53 WRN] SRTP unprotect failed for audio, result -1.
[17:15:53 WRN] SRTP unprotect failed for audio, result -1.
[17:15:53 INF] Data channel got message: WFNAL
[17:15:53 WRN] SRTP unprotect failed for audio, result -1.
[17:15:53 WRN] SRTP unprotect failed for audio, result -1.
[17:15:53 DBG] DTLS client received close notification: warning(1), close_notify(0).
[17:15:53 DBG] SCTP closing transport as a result of DTLS close notification.
[17:15:53 DBG] DTLS client raised close notification: warning(1), close_notify(0).
[17:15:53 DBG] DTLS client raised close notification: warning(1), close_notify(0).

@jimm98y
Copy link
Collaborator Author

jimm98y commented Jan 11, 2026

aiortc test should be fixed. My DigestCertificateUtils.IsHashSupported was returning true only for SHA-256, but the certificate presented by that Docker container was using SHA-512.

@sipsorcery
Copy link
Member

aiortc test should be fixed. My DigestCertificateUtils.IsHashSupported was returning true only for SHA-256, but the certificate presented by that Docker container was using SHA-512.

Yes, aiortc test is able to establish a DTLS connection now.

@jimm98y
Copy link
Collaborator Author

jimm98y commented Jan 11, 2026

As for the "sipsorcery master test", I think I found the problem and it seems it's a bug in sipsorcery master.

DtlsSrtpClient is requesting the use of MKI which it calculates in a strange way:

byte[] mki = new byte[(SrtpParameters.SRTP_AES128_CM_HMAC_SHA1_80.GetCipherKeyLength() + SrtpParameters.SRTP_AES128_CM_HMAC_SHA1_80.GetCipherSaltLength()) / 8];
random.NextBytes(mki); // Reusing our secure random for generating the key.
this.clientSrtpData = new UseSrtpData(protectionProfiles, mki);

Why is it strange? SrtpParameters.SRTP_AES128_CM_HMAC_SHA1_80.GetCipherKeyLength() returns the key length in bytes already, yet it is being divided by 8 once more. This produces number 3, which means the client will generate MKI the size of 3 bytes. Most implementations I've seen so far were using fixed size of 4 bytes or less, this being 3 is rather uncommon.

Second, this MKI is sent to the server to negotiate DTLS-SRTP, which means the server is supposed to use it in all SRTP/SRTCP messages to identify the master keys being used. This is actually what is happening, I can see the server is appending the MKI right before the auth tag (see https://www.rfc-editor.org/rfc/rfc3711#section-3.1).

Now the SRTP authentication is failing in sipsorcery master (error code -1) and it is because the Unprotect implementation does not understand the optional MKI in the message (which it had negotiated in the DTLS UseSrtp extension) and it is including it in the authenticated portion of the packet, which is wrong according to RFC 3711. Those 3 bytes should be stripped from the message along with the auth tag like so:

// get original authentication and store in tempStore
pkt.ReadRegionToBuff(pkt.GetLength() - tagLength, tagLength, tempStore);

pkt.shrink(tagLength + (SrtpParameters.SRTP_AES128_CM_HMAC_SHA1_80.GetCipherKeyLength() + SrtpParameters.SRTP_AES128_CM_HMAC_SHA1_80.GetCipherSaltLength()) / 8);

// save computed authentication in tagStore
AuthenticatePacketHMCSHA1(pkt, guessedROC);

The best fix for the current master branch would be to not negotiate MKI at all (like most of the other WebRTC implementations I've seen) and set it to empty array:

byte[] mki = new byte[0];
this.clientSrtpData = new UseSrtpData(protectionProfiles, mki);

This should fix the errors.

@sipsorcery
Copy link
Member

Now the SRTP authentication is failing in sipsorcery master (error code -1) and it is because the Unprotect implementation does not understand the optional MKI in the message (which it had negotiated in the DTLS UseSrtp extension) and it is including it in the authenticated portion of the packet, which is wrong according to RFC 3711.

Thanks for the analysis.

So it works on master now because the DTLS server implementation ignores the mki and doesn't send it to the client?

It seems a bit strange that the current master DTLS client implementation, that is setting the mki, doesn't get the errors with any of the other WebRTC implementations. Shouldn't they also be sending the mki back to the DTLS client and would thus trigger the same error message?

@jimm98y
Copy link
Collaborator Author

jimm98y commented Jan 12, 2026

"It seems a bit strange that the current master DTLS client implementation, that is setting the mki, doesn't get the errors with any of the other WebRTC implementations."
Yes, I think there is a specific requirement for WebRTC as stated in RFC 8827:

An SRTP Master Key Identifier (MKI) MUST NOT be used.

So I suppose if those other implementations are pure WebRTC, then they are correctly ignoring the MKI in the client's request. Nonetheless I think the client should not have asked for MKI according to the RFC 8827, so it is also a bug of the current sipsorcery master.

As for SharpSRTP, it simply implements SRTP and DTLS-SRTP, not WebRTC. However, I think I might offer some way of telling the server to ignore any MKI to make the SRTP server behave as the other WebRTC implementations...

Edit: I added ForceDisableMKI property and set it to true in sipsorcery, making it ignore MKI like the other WebRTC implementations.

@sipsorcery
Copy link
Member

Thanks for providing that reference, that's great. I've created #1489 to remove MKI. It's trivial but perhaps if you could verify it's all that's required I'd appreciate it.

@sipsorcery
Copy link
Member

I've tested with the mki fix and the test with master as the server and this PR as the client for the echo test now works without errors (not the DTLS roles are the reverse of the echo test ones).

I've also run the tests with all the echo test client options from the webrtc-interop repo. They all work well except the sipsorcery master branch one which does successfully neogtiate DLS connection but then throws the exception below when closing.

[20:44:42 DBG] RTCPeerConnection DTLS handshake result True, is handshake complete True.
[20:44:42 DBG] RTCPeerConnection remote certificate fingerprint matched expected value of 93:4D:DE:24:C9:9C:02:40:8E:A6:5F:8C:18:36:38:10:7C:47:A2:E3:83:EE:7C:B3:ED:ED:E6:95:CB:EB:92:40 for sha-256.
[20:44:42 INF] Peer connection state changed to connected.
[20:44:42 DBG] Peer connection closed with reason normal.
[20:44:42 DBG] Data channel with id null has been closed
[20:44:42 DBG] RtpIceChannel for [::]:51352 closed.
[20:44:42 DBG] DTLS server raised close notify: warning(1), close_notify(0).
Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at SIPSorcery.Net.SrtcpCryptoContext.ComputeIv(Byte label)
   at SIPSorcery.Net.SrtcpCryptoContext.DeriveSrtcpKeys()
   at SIPSorcery.Net.SrtcpTransformer.Transform(Byte[] pkt, Int32 offset, Int32 length)
   at SIPSorcery.Net.DtlsSrtpTransport.ProtectRTCP(Byte[] packet, Int32 offset, Int32 length)
   at SIPSorcery.Net.DtlsSrtpTransport.ProtectRTCP(Byte[] payload, Int32 length, Int32& outLength)
   at SIPSorcery.Net.MediaStream.SendRtcpReport(Byte[] reportBuffer)
   at SIPSorcery.Net.MediaStream.SendRtcpReport(RTCPCompoundPacket report)
   at SIPSorcery.Net.RTPSession.SendRtcpReport(SDPMediaTypesEnum mediaType, RTCPCompoundPacket report)
   at SIPSorcery.Net.RTCPSession.Close(String reason)
   at SIPSorcery.Net.RTPSession.CloseRtcpSession(MediaStream mediaStream, String reason)
   at SIPSorcery.Net.RTPSession.CloseMediaStream(String reason, MediaStream mediaStream)
   at SIPSorcery.Net.RTPSession.Close(String reason)
   at SIPSorcery.Net.RTCPeerConnection.Close(String reason)
   at SIPSorcery.Net.RTCPeerConnection.close()
   at webrtc_echo.Program.Main(String[] args) in /src/client/Program.cs:line 192
   at webrtc_echo.Program.<Main>(String[] args)
/client.sh: line 2:     7 Aborted                 (core dumped) /usr/bin/dotnet /app/webrtc-echo-client.dll $1

The test commands were:

Echo test server - webrtccmdline from this PR branch
sipsorcery\examples\webrtccmdline>dotnet run --echoserver

Echo test client (image built from sipsorcery master version 10.0.2):
docker run --entrypoint "/client.sh" ghcr.io/sipsorcery/sipsorcery-webrtc-echo http://host.docker.internal:8080/offer

@jimm98y
Copy link
Collaborator Author

jimm98y commented Jan 13, 2026

Looks like an issue in the sipsorcery master to me, at least partially.

One part of the problem were the NULL cipher suites. My client/server offered them as supported and they were incorrectly selected by the server (sipsorcery master), which exposed a bug in my code where NULL cipher suites failed to derive the master key/salt. This problem is now fixed and NULL cipher suites seem to be working when negotiated.

However, just fixing it was not enough - according to RFC 8827:

Media traffic MUST NOT be sent over plain (unencrypted) RTP or RTCP; that is, implementations MUST NOT negotiate cipher suites with NULL encryption modes.

I fixed this on my side where I disabled NULL cipher suited for both the client and the server. However, the fact that they were selected by the server despite being offered last led me to finding another bug in the current sipsorcery master. ProcessClientExtensions in DtlsSrtpServer should be modified to pick the first supported cipher suite, not the last one. The cipher suites are listed from the most preferred one to the least preferred one as per RFC 5764:

The SRTPProtectionProfiles list indicates the SRTP protection
profiles that the client is willing to support, listed in descending
order of preference.

I think that the code in DtlsSrtpServer.ProcessClientExtensions in sipsorcery master branch should look as follows:

// profiles are listed in descending order of preference https://datatracker.ietf.org/doc/html/rfc5764#section-4.1.1
bool isProfileSelected = false;
foreach (int profile in clientSrtpData.ProtectionProfiles)
{
    switch (profile)
    {
        case SrtpProtectionProfile.SRTP_AES128_CM_HMAC_SHA1_80:
        case SrtpProtectionProfile.SRTP_AES128_CM_HMAC_SHA1_32:
            chosenProfile = profile;
            isProfileSelected = true;
            break;
    }
    if (isProfileSelected)
    {
        break;
    }
}

This would ensure the first matching profile is chosen as well as all NULL profiles are skipped.

@sipsorcery
Copy link
Member

Thanks again for the awesome diagnosis. I'll do some testign with that suggestion.

@sipsorcery
Copy link
Member

I've created #1492 to fix the null profiles and incorrect selection logic. If you have a chance to review and confirm that'd be great.

@sipsorcery
Copy link
Member

All my tests are now good. I expect there will be a few more edge cases that come out but if you're happy to merge I'm good to go.

@jimm98y
Copy link
Collaborator Author

jimm98y commented Jan 15, 2026

Thank you very much for running all the tests and helping me to improve the code! Yes, I'd expect a few more edge cases as well, especially the ones related to the now deprecated old DTLS 1.0. I think we can merge it now and I'll keep looking for new issues being reported here.

@sipsorcery sipsorcery merged commit 91e653c into sipsorcery-org:master Jan 15, 2026
@sipsorcery
Copy link
Member

All green on the interop tests with the other WebRTC implementations post merge!

image

#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
public void Send(ReadOnlySpan<byte> buf)
{
OnDataReady?.Invoke(buf.ToArray());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jimm98y,

Besides the usage of the expensive Enumerable.ToArray(), shouldn't this be slicing to off..int?

Something like:

Suggested change
OnDataReady?.Invoke(buf.ToArray());
OnDataReady?.Invoke(buf.AsSpan(off, len).ToArray());

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general yes, you are right, but looking at the stack trace it seems it's always used with 0-length which is why it works "as is". However, I agree that if somebody else were to call this method, he might be surprised of the current behavior.


public DtlsTransport Transport { get; private set; }
private ConcurrentQueue<byte[]> _data = new ConcurrentQueue<byte[]>();
private string _remoteEndPoint;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not used.

Suggested change
private string _remoteEndPoint;

{
_chunks.Add(buf);
}
_remoteEndPoint = remoteEndPoint;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not used.

Suggested change
_remoteEndPoint = remoteEndPoint;

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah! That's a leftover from the HelloVerify request, which has been disabled for WebRTC because Firefox did not support it. I'll remove it, thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants